package com.cfushn.ddd.tools.crypto;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Optional;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import lombok.extern.slf4j.Slf4j;

/**
 * ShaUtil
 * <p>
 * sha256的升级版 -> pbkdf2
 *
 * @author CFuShn 2022/11/22 11:08
 */
@Slf4j
@SuppressWarnings("unused")
public class ShaUtil {

    /**
     * 盐的长度
     */
    public static final int SALT_BYTE_SIZE = 16;

    /**
     * 生成密文的长度(例：64 * 4，密文长度为64)
     */
    public static final int HASH_BIT_SIZE = 64 * 4;

    /**
     * 默认迭代次数
     * 在条件: PBKDF2&SHA256算法,16字节盐,256位密文长度 的指标中, 本地(i7 10750H)睿频测试时间如下
     * //10万及以下 2S
     * //100万 3.4S
     * //1000万 15S
     */
    private static final Integer DEFAULT_ITERATIONS = 1000;

    /**
     * 算法名称
     */
    private static final String ALGORITHM = "PBKDF2WithHmacSHA256";

    /**
     * 获取密文(默认迭代次数,不指定盐)
     *
     * @param password 明文密码
     * @return String
     */
    public static String encode(String password) {
        return encode(password, getSalt());
    }

    /**
     * 获取密文(指定迭代次数,不指定盐)
     *
     * @param password   明文密码
     * @param iterations 迭代次数
     * @return String
     */
    public static String encode(String password, int iterations) {
        return encode(password, getSalt(), iterations);
    }

    /**
     * 获取密文(默认迭代次数,指定盐)
     * 默认迭代次数：2000
     *
     * @param password 明文密码
     * @param salt     盐值
     * @return String
     */
    public static String encode(String password, String salt) {
        return encode(password, salt, DEFAULT_ITERATIONS);
    }

    /**
     * 获取密文(指定迭代次数,指定盐)
     * <p>
     * 注：此方法
     * 不需要的直接用getEncodedHash方法返回的密文
     *
     * @param password   密码明文
     * @param salt       加盐
     * @param iterations 迭代次数
     * @return hexString
     */
    public static String encode(String password, String salt, int iterations) {
        //返回密文字符串组成：算法名称+迭代次数+盐值+密文
        return String.format("%d$%s$%s", iterations, salt,
            getEncodedHash(password, salt, iterations));
    }

    /**
     * 获取密文(指定迭代次数,指定盐)
     *
     * @param password   密码明文
     * @param salt       加盐
     * @param iterations 迭代次数
     * @return toHex String
     */
    public static String getEncodedHash(String password, String salt, int iterations) {
        // Returns only the last part of whole encoded password
        SecretKeyFactory keyFactory = null;
        try {
            keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            log.error("Could NOT retrieve PBKDF2WithHmacSHA256 algorithm", e);
        }
        KeySpec keySpec = new PBEKeySpec(password.toCharArray(),
            salt.getBytes(StandardCharsets.UTF_8), iterations,
            HASH_BIT_SIZE);
        SecretKey secret = null;
        try {
            assert keyFactory != null;
            secret = keyFactory.generateSecret(keySpec);
        } catch (InvalidKeySpecException e) {
            log.error("Could NOT generate secret key", e);
        }

        // //使用Base64进行转码密文
        // byte[] rawHash = secret.getEncoded();
        // byte[] hashBase64 = Base64.getEncoder().encode(rawHash);
        // return new String(hashBase64);

        // 使用十六进制密文
        return Optional.ofNullable(secret).map(SecretKey::getEncoded).map(ShaUtil::toHex)
            .orElseThrow(() -> new RuntimeException("Could NOT generate secret key"));
    }

    /**
     * 二进制字符串转十六进制字符串
     *
     * @param array 二进制数组
     * @return hex
     */
    private static String toHex(byte[] array) {
        BigInteger bi = new BigInteger(1, array);
        String hex = bi.toString(16);
        int paddingLength = (array.length * 2) - hex.length();
        if (paddingLength > 0) {
            return String.format("%0" + paddingLength + "d", 0) + hex;
        } else {
            return hex;
        }
    }

    /**
     * 十六进制字符串转二进制字符串
     *
     * @param hex 十六进制字符串
     * @return byte[] binary
     */
    private static byte[] fromHex(String hex) {
        byte[] binary = new byte[hex.length() / 2];
        for (int i = 0; i < binary.length; i++) {
            binary[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
        }
        return binary;
    }

    /**
     * 密文加盐     (获取‘SALT_BYTE_SIZE’长度的盐值)
     *
     * @return salt
     */
    public static String getSalt() {
        //盐值使用ASCII表的数字加大小写字母组成
        int length = SALT_BYTE_SIZE;
        SecureRandom secureRandom = new SecureRandom();
        char[] rs = new char[length];
        for (int i = 0; i < length; i++) {
            int t = secureRandom.nextInt(3);
            if (t == 0) {
                rs[i] = (char) (secureRandom.nextInt(10) + 48);
            } else if (t == 1) {
                rs[i] = (char) (secureRandom.nextInt(26) + 65);
            } else {
                rs[i] = (char) (secureRandom.nextInt(26) + 97);
            }
        }
        return new String(rs);
    }

    /**
     * 验证密码
     *
     * @param password       明文
     * @param hashedPassword 密文
     * @return 匹配(true), 不匹配(false)
     */
    public static boolean verification(String password, String hashedPassword) {
        //hashedPassword = 迭代次数+盐值+密文;
        String[] parts = hashedPassword.split("\\$");
        if (parts.length != 3) {
            return false;
        }
        //解析得到迭代次数和盐值进行盐值
        int iterations = Integer.parseInt(parts[0]);
        String salt = parts[1];
        String hash = encode(password, salt, iterations);
        return hash.equals(hashedPassword);
    }

    // 测试
    public static void main(String[] args) throws Exception {

        System.out.println("Start");
        long startTime = System.nanoTime();

        //获取密文（密码加盐）
        String t = encode("sim@2013");
        long endTime = System.nanoTime();
        long duration = (endTime - startTime);
        System.out.println("encode===" + t);
        System.out.println("耗时(s)===" + duration / 1000000000.0);

        boolean verification = verification("sim@2013", t);
        boolean verification2 = verification("S0im@2013", t);
        System.out.println(verification);
        System.out.println(verification2);

        String oldPassword7 =
            "PBKDF2&SHA256$2000$SzNgPdzz$50f22e207abec8e837bce97642a46f965f19d992217d7df9be496700b286345d";
        String oldPassword8 =
            "PBKDF2&SHA256$2000$VzmO4yOZ$71891148cfbdd9103aaa511d20dc52431c8947ce4a00d89708231ec76053f6f3";
        String oldPassword9 =
            "PBKDF2&SHA256$2000$3xuRb8AR$6bff0310fd35c88572633b00d36e9039fef3e68c6e37b14204958946e8738e93";
        boolean verification7 = verification("admin123456", oldPassword7);
        boolean verification8 = verification("admin123456", oldPassword8);
        boolean verification9 = verification("admin123456", oldPassword9);
        System.out.println(verification7);
        System.out.println(verification8);
        System.out.println(verification9);
    }
}
